# peppy Copyright (c) 2006-2008 Rob McMullen
# MIT Licensed; see http://peppy.flipturn.org for more info
"""
Major mode for viewing the Mandelbrot set using Ned Batchelder's Aptus code.

I was reading L{Prime
Obsession<http://www.johnderbyshire.com/Books/Prime/page.html>} and it
prompted me to look into complex numbers again.  One of the interesting things
about the L{Mandelbrot set<http://en.wikipedia.org/wiki/Mandelbrot_set>} is
the simplicity of the equation behind the fractal image.

My first attempt at creating a fractal major mode was with a version
from the L{Mandelbrot example from the NumPy tutorial<http://www.scipy.o
rg/Tentative_NumPy_Tutorial/Mandelbrot_Set_Example>}.  In thinking
about coding up a faster C version, I did the obligatory google
search to see if anyone else had thought about this.  I discovered
U{Aptus<http://nedbatchelder.com/code/aptus/index.html>}, pretty much exactly
what I was going to try to implement.

So, naturally, rather than re-inventing something that was already a well-
tested program, I just include it here.  Ah, open source!

Note that Aptus is licensed under the MIT license, so unlike the rest of peppy
which is licensed under the GPL, this plugin respects the Aptus license and is
also licensed under the MIT license.
"""

import os, random, time, math
from cStringIO import StringIO

import wx
import wx.stc

from peppy.about import AddCredit
from peppy.debug import *
from peppy.actions import *
from peppy.major import *
from peppy.stcinterface import *
import peppy.vfs as vfs

from aptus.options import AptusState

AddCredit("Ned Batchelder", "for the Mandelbrot set fractal viewer from <a href=\"http://nedbatchelder.com/code/aptus/\">Aptus</a>")

icondict = {
'aptus.png':
'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x10\x00\x00\x00\x10\x08\x06\
\x00\x00\x00\x1f\xf3\xffa\x00\x00\x00\x04sBIT\x08\x08\x08\x08|\x08d\x88\x00\
\x00\x02\x04IDAT8\x8d\x8d\x91\xb1K[Q\x14\xc6\x7f\xf7\x19\xf3\xb8 D\xb2\xb8\
\xf8($\x0e\x0f\x8b\x08\xa2\x85d\xd1\x82\x10\x9a\xc1\xa1\x15\x02\x0f\xa1\xc5\
\xadB\xb7 n\x97L\x8ak\xfb\x07ts\x08q\x0bql\xc1\x0e>A\x10i\xf6\x12\x03\xaf\
\x10\x8cC\xe0\xe1\x15r:4\x11L\xa3\xf4\x83\x03\x87\x8f\xef|\xe7\xbb\xf7(F`\
\xc4L\x02-`f\xc1.p\x95\xbc\x02\xf8\rx\x15U\xb9\x1f\xd5;\xa3\x04\xf0\x1a\x98\
\x01H\'\xd2Cnf\xc0\xff\x83q\x06\xef\x01\xf2\xe4q~8\xe4\xc9?\xe2\x9f50b\x96S\
\xa4J%J$N\x13\xec|\xdc!q\x9a\xa0D\x89\x14\xa9\x92\x11\xb3\xfc\xa4\x81\x11\
\x93\x06\x8ez\xf4&\xba\xcd.\xc5\xf5"\xcd\x9fM\x8a\xebE\xba\xcd.=z\x13\xc0\
\xd1@\xf7\xd8\xc0\x88I\x025`\xaeO\x1f\xebX\xac\xb5\x00Xk\xb1\x8e\xa5O\x1f`\
\x0e\xa8\r\xf4\x8f\x12|\x01\xd6\x00\\\\|\xdf\xa7\xbc[\xc6u]\xca\xbbe|\xdf\
\xc7\xc5\x1d\xce\xac\r\xf4\x00(#\xe6-PsqYd\x11}\xae\xd9\xfb\xb4\xc7yx\x8e\
\x88\xa0\x94b\xe5\xd5\n\x07\x9f\x0f\x88Wb.\xb9\xe4\x8e;\x80w\x15U9v\x80C\x80\
$I\xe2\xe3\x98`# <\x0b\x11\x11\x00D\x84\xf0,$\xd8\x08\x88\x8fc\x92<\xa4?\x1c\
>!\xab\xd1\xf8\xf84N\x1aDQ4\xeeZDQD\xe3\xa4\x81\x8f\x8fF\x03d\x01\xa8\xf6\
\xab\x12\xde\x86\x12l\x05\x92\xc9d\x04x\xb22\x99\x8c\x04[\x81\x84\xb7\xa1T\
\xfbU\x01p:\xaa\x83\x12\x857\xeb\xa1\xb5\x1e\xbb}\x08\xad5\xde\xac\x87\x12EG\
u\xfe^!"\xba\xaeO\xd7\xc9\xeeg)\x14\x0b\xcf\x1a\x14\x8a\x05\xb2\xfbY\xea\xd3\
u"\xa2\xeb\xe1\x1fl\x0bb\xdb\xb4\xc9m\xe6\x98\x7f9\xff00\xda\xe76s\xb4i#\x88\
\x05\xb6\x01\x14\x80\x11\xb36\xc9\xe4\xd7%Yz!\x08\xadF\x8b\x1b}\xc3\xd4\xea\
\x14\xbd\xef=\xd2q\x1a\xef\x8d\x87Bq\xa1.~\xdds\xff\xa1\xa2*\xdf\x9e\x8d\xfb\
\xbf\xf8\x034\xe8\xc5\xa23Bw\xc4\x00\x00\x00\x00IEND\xaeB`\x82' 
}
from peppy.lib.iconstorage import *
addIconsFromDict(icondict)


class AptusFS(vfs.BaseFS):
    """Filesystem to recognize "aptus:fractal_name" URLs
    
    This simple filesystem allows URLs in the form of "aptus:fractal_name",
    and provides the mapping from the fractal name to the class that draws the
    fractal.  This is used by the L{AptusSTC} to return the fractal class when
    requested by L{AptusApp.get_fractal_class}
    """
    fractals = {}
    
    @classmethod
    def register(cls, fractal_name):
        cls.fractals[fractal_name] = "-*- AptusMode -*-\nclass = %s\n" % fractal_name
    
    @classmethod
    def _get(cls, reference):
        fractal_name = str(reference.path)
        if fractal_name in cls.fractals:
            return cls.fractals[fractal_name]
        return ""
    
    @classmethod
    def exists(cls, reference):
        return bool(cls._get(reference))

    @classmethod
    def is_file(cls, reference):
        return bool(cls._get(reference))

    @classmethod
    def is_folder(cls, reference):
        return False

    @classmethod
    def can_read(cls, reference):
        return bool(cls._get(reference))

    @classmethod
    def can_write(cls, reference):
        return False

    @classmethod
    def get_size(cls, reference):
        return len(cls._get(reference))

    @classmethod
    def open(cls, reference, mode=None):
        text = cls._get(reference)
        if text:
            fh = StringIO(text)
            #dprint(fh.getvalue())
            return fh
        return None

AptusFS.register('mandelbrot')


class AptusMode(wx.Panel, MajorMode):
    """
    Major mode for viewing fractals using Ned Batchelder's Aptus engine.
    """
    debuglevel=0

    keyword="Aptus"
    icon='aptus.png'

    @classmethod
    def verifyProtocol(cls, url):
        if url.scheme == 'aptus':
            return True
        return False

    @classmethod
    def verifyMagic(cls, header):
        return "Aptus State" in header and "iter_limit" in header

    def __init__(self, parent, wrapper, buffer, frame):
        MajorMode.__init__(self, parent, wrapper, buffer, frame)
        wx.Panel.__init__(self, parent)
        sizer = wx.BoxSizer(wx.VERTICAL)
        
        # Defer loading the numpy-dependent aptus code until now so that numpy
        # isn't loaded immediately
        try:
            import aptus_panel
            self.aptus = aptus_panel.AptusPanel(self, self)
            sizer.Add(self.aptus, 1, wx.EXPAND)
            self.SetSizer(sizer)
            self.Layout()
        except ImportError, e:
            if 'numpy' in str(e):
                raise MajorModeLoadError("Aptus needs NumPy, the Python module for scientific computing that provides fast\nmathematical operations on arrays.\n\nInstall from http://numpy.scipy.org")
            else:
                raise
    
    def showInitialPosition(self, user_url, options=None):
        """Calculate the initial display of the fractal
        
        This method is called once, at major mode creation time, to set up
        the first display of the fractal.  I put this here rather than in
        the __init__ method because it allows the user interface to be fully
        generated before calculating alll the iterations.
        
        Also, I'm currently using this to select which fractal is displayed by
        the fragment of the url.
        """
        self.parseAptusState()
        self.aptus.set_view()
    
    def parseAptusState(self):
        text = self.buffer.stc.GetText()
        if self.verifyMagic(text):
            self.dprint(text)
            state = AptusState(self.aptus)
            state.read_string(text)
    
    def updateText(self):
        state = AptusState(self.aptus)
        text = state.write_string()
        self.buffer.stc.SetText(text)
        self.buffer.stc.SetSavePoint()
        self.buffer.modified = False
        self.dprint(text)
    
    def savePreHook(self, url):
        self.updateText()
        self.parseAptusState()
    
    def setViewPositionData(self, data):
        # This is used to restore the state of the fractal to whatever is in
        # the text saved state
        self.parseAptusState()
        self.aptus.set_view()


##### Global Actions

class ViewAptus(SelectAction):
    """Start the Aptus Mandelbrot viewer
    """
    name = "Aptus Mandelbrot Set"
    tooltip = "View the Mandelbrot Set using the Aptus viewer"
    default_menu = "Tools/Games"

    def action(self, index=-1, multiplier=1):
        self.frame.open("aptus:mandelbrot")

class SavedLocations(OnDemandGlobalListAction):
    """Go to a previously saved location in the fractal.
    
    Maintains a list of saved locations so you can go back to view them.
    """
    name = "Saved Locations"
    default_menu = (("Aptus/Saved Locations", -800), 0)
    inline = True
    
    # Default list of saved locations if no save file exists
    jumps = [
        ((-0.5,0.0), (3.0,3.0)),
        ((-1.8605294939875601,-1.0475516319329809e-005), (2.288818359375e-005,2.288818359375e-005)),
        ((-1.8605327731370924,-1.2700557708795141e-005), (1.7881393432617188e-007,1.7881393432617188e-007)),
        ((0.45687170535326038,0.34780396997928614), (0.005859375,0.005859375)),
    ]
    
    # Current list is set to the default list; will point to the list of saved
    # locations if a save file is found
    storage = jumps
    
    def action(self, index=-1, multiplier=1):
        assert self.dprint("opening location %s" % (str(self.storage[index])))
        self.mode.aptus.center, self.mode.aptus.diam = self.storage[index]
        self.mode.aptus.set_view()

class AddToSavedLocations(SelectAction):
    """Add the current view to the saved locations"""
    name = "Add To Saved Locations"
    default_menu = ("Aptus/Saved Locations", -900)


##### Plugin definition

class AptusPlugin(IPeppyPlugin, debugmixin):
    """Peppy plugin that registers the Mandelbrot viewer and its actions.
    """
    default_classprefs = (
        StrParam('save_file', 'aptus-save.dat', 'Filename within the peppy configuration directory to store persistent data like the saved locations.'),
        )
    
    def activateHook(self):
        vfs.register_file_system('aptus', AptusFS)

    def initialActivation(self):
        pathname = wx.GetApp().getConfigFilePath(self.classprefs.save_file)
        if not os.path.exists(pathname):
            return
        try:
            import pickle
            
            fh = open(pathname, 'rb')
            data = pickle.load(fh)
            if isinstance(data, list):
                SavedLocations.storage = data
        except:
            dprint("Failed loading Aptus data from %s" % pathname)
            pass
    
    def requestedShutdown(self):
        pathname = wx.GetApp().getConfigFilePath(self.classprefs.save_file)
        try:
            import pickle
            
            fh = open(pathname, 'wb')
            pickle.dump(SavedLocations.storage, fh)
        except:
            dprint("Failed saving Aptus data from %s" % pathname)
            pass

    def deactivateHook(self):
        vfs.deregister_file_system('aptus')

    def getMajorModes(self):
        yield AptusMode
    
    def getCompatibleActions(self, modecls):
        if issubclass(modecls, AptusMode):
            # Delay loading Aptus specific actions until an AptusMode is
            # actually requested.
            import aptus_actions
            actions = aptus_actions.actions()
            actions.extend([SavedLocations, AddToSavedLocations])
        else:
            actions = []
        actions.append(ViewAptus)
        return actions
